# vim:encoding=utf8
from twisted.internet import reactor
from zope.interface import implements
from twisted.python import log
from twisted.internet import defer
from twisted.internet.protocol import ReconnectingClientFactory
from twisted.web.client import _parse
from twisted.web2.responsecode import MOVED_PERMANENTLY, FOUND, SEE_OTHER, USE_PROXY, TEMPORARY_REDIRECT, RESPONSES
from twisted.web2 import stream as stream_mod
from twisted.web2.client.http import HTTPClientProtocol, ClientRequest
from twisted.web2.client.interfaces import IHTTPClientManager

#web2client
from ungzip import *
from stream import *

__VERSION__ = '0.01-'
DEFAULT_USER_AGENT = 'web2client/%s (+http://code.google.com/p/twisted-web2-client/)' % __VERSION__

class ReconnectingHTTPClientManager(ReconnectingClientFactory):
    implements(IHTTPClientManager)

    protocol = HTTPClientProtocol

    #ReconnectingClientFactory declare
    #self.maxRetries
    #self.retries

    def __init__(self, maxRetries=0):
        self.maxRetries = maxRetries
        self.stack      = []
        self.deferred   = defer.Deferred()

    #IHTTPClientManager
    def clientBusy(self, proto):
        print 'clientBusy', proto

    #IHTTPClientManager
    def clientIdle(self, proto):
        print 'clientIdle', proto

        while self.stack:
            request, deferred = self.stack.pop(0)
            closeAfter = len(self.stack) == 0
            d = proto.submitRequest(request, closeAfter=closeAfter)
            d.addCallbacks(deferred.callback, deferred.errback)
        #manager finish
        self.deferred.callback(True)

    #IHTTPClientManager
    def clientPipelining(self, proto):
        print 'clientPipelining', proto

    #IHTTPClientManager
    def clientGone(self, proto):
        print 'clientGone', proto

    def startSubmit(self, proto):
        print 'startSubmit', proto

        #一つだけリクエストを取得する
        try:
            request, deferred = self.stack.pop(0)
        except:
            print 'request not come yet'
            reactor.callLater(1, self.startSubmit, proto)
            return 

        closeAfter = len(self.stack) == 0

        d = proto.submitRequest(request, closeAfter=closeAfter)
        d.addCallbacks(deferred.callback, deferred.errback)

    def pushRequest(self, request):
        print 'pushRequest', request

        assert isinstance(request, ClientRequest), 'pushed request must be instance of twisted.web2.client.ClientRequest'
        deferred = defer.Deferred()
        self.stack.append((request, deferred))
        return deferred

    #ReconnectingClientFactory
    def startedConnection(self):
        print 'startedConnection'

    #Factory
    def buildProtocol(self, addr):
        print 'buildProtocol', addr

        p = self.protocol(self)
        p.factory = self
        reactor.callLater(0, self.startSubmit, p)
        self.resetDelay()
        return p

    #ReconnectingClientFactory
    def clientConnectionFailed(self, connector, reason):
        print 'clientConnectionFailed', connector
        if self.retries == self.maxRetries:
            reactor.callLater(0, self.deferred.errback, reason)
            return
        #retry
        self.continueTrying = len(self.stack) >  0
        ReconnectingClientFactory.clientConnectionFailed(self, connector, reason)

    #ReconnectingClientFactory
    def clientConnectionLost(self, connector, reason):
        print 'clientConnectionLost', connector
        if self.retries == self.maxRetries:
            reactor.callLater(0, self.deferred.errback, reason)
            return
        #retry
        self.continueTrying = len(self.stack) >  0
        ReconnectingClientFactory.clientConnectionLost(self, connector, reason)

class Broker(object):
    client = ReconnectingHTTPClientManager

    def __init__(self):
        self.actives = {}

    def ask(self, uri, method='GET', headers=None, stream=None, proxies=None, *args, **kwargs):
        scheme, host, port, path = _parse(uri)
        print 'ask', (host, port, path)

        if headers == None:
            headers = {}
        headers['Host'] = host
        headers.setdefault('User-Agent', DEFAULT_USER_AGENT)

        #TODO:understand http_headers
        from twisted.web2 import http_headers
        headers = http_headers.Headers(headers)
        def _plaingenerator(x):
            yield x
        headers.handler.addGenerators('accept-encoding', [_plaingenerator])

        #change to proxied request
        if proxies and scheme in proxies:
            scheme, host, port, path = _parse(proxies[scheme])
            path = uri
            print 'via proxy', (host, port, uri)

        request = ClientRequest(method, path, headers, stream)

        if (host, port) not in self.actives:
            manager = self.client(1)
            if scheme == 'https':
                from twisted.internet import ssl
                if contextFactory is None:
                    contextFactory = ssl.ClientContextFactory()
                reactor.connectSSL(host, port, manager, contextFactory, *args, **kwargs)
            else:
                reactor.connectTCP(host, port, manager, *args, **kwargs)
            self.actives[(host, port)] = manager
        else:
            manager = self.actives[(host, port)]
            if manager.deferred.called:
                del self.actives[(host, port)]
                return self.ask(uri, method, headers, stream, proxies, *args, **kwargs)
        return manager.pushRequest(request)

class ExtensiveBroker(Broker):
    def ask(self, uri, method='GET', headers=None, stream=None, proxies=None, autoRedirect=True, compressResponse=True, *args, **kwargs):
        if autoRedirect:
            if stream is not None:
                stream = BufferedProxyStream(stream)
        if compressResponse:
            if headers is None:
                headers = {}
            headers.setdefault('accept-encoding', 'gzip,deflate')
        d = super(ExtensiveBroker, self).ask(uri, method, headers, stream, proxies, *args, **kwargs)
        if compressResponse:
            def _wrap_stream(resp):
                if resp.headers:
                    try:
                        enc = resp.headers.getHeader('Content-Encoding')[0]
                        if enc == 'gzip':
                            resp.stream = ungzipStream(resp.stream)
                            print 'gzipstream', resp.stream
                        elif enc == 'deflate':
                            resp.stream = inflateStream(resp.stream)
                    except:
                        pass
                return resp
            d.addCallback(_wrap_stream)
        if autoRedirect:
            d.addCallback(self.check30xRedirect, method, headers, stream, proxies, *args, **kwargs)
        return d

    def check30xRedirect(self, resp, method, headers, stream, proxies, *args, **kwargs):
        if resp.code in (MOVED_PERMANENTLY, FOUND, SEE_OTHER, USE_PROXY, TEMPORARY_REDIRECT):
            location = resp.headers.getHeader('location')
            print 'Redirect code:%i message:%s location:%s' % (resp.code, RESPONSES[resp.code], location)
            if resp.code in (MOVED_PERMANENTLY, FOUND, SEE_OTHER):
                method = 'GET'
                stream = None
            elif resp.code == USE_PROXY:
                #XXX:throw
                return resp
            elif resp.code == TEMPORARY_REDIRECT:
                if stream:
                    stream = stream.data
            return self.ask(location, method, headers, stream, proxies, True, *args, **kwargs)
        return resp

#TODO
#1.If-Modified-Since Etag などを対応するアプリケーションレベルな奴
#2.もっとアプリケーションレベル、ヒストリを保持してbackやforwardを実装してるとかキャッシングしてるとかBrowsingContext

def getResponseBody(resp):
    d = defer.Deferred()
    stack = []
    stream_mod.readStream(resp.stream, stack.append).addCallback(lambda x: ''.join(stack)).addCallbacks(d.callback, d.errback)
    return d

if __name__ == '__main__':
    import sys
    log.startLogging(sys.stdout)

    broker = ExtensiveBroker()

    l = []
    for uri in sys.argv[1:]:
        l.append(broker.ask(uri).addCallback(getResponseBody))
    dl = defer.DeferredList(l)
    dl.addBoth(lambda x: sys.stdout.write(str(x) + '\n'))
    dl.addBoth(lambda x: reactor.stop())
    reactor.run()
